Skip to content

ADD: UInt256 implementation of evm's op MOD#9188

Merged
ahamlat merged 27 commits intobesu-eth:mainfrom
thomas-quadratic:feature/uint256-op-modulus
Oct 13, 2025
Merged

ADD: UInt256 implementation of evm's op MOD#9188
ahamlat merged 27 commits intobesu-eth:mainfrom
thomas-quadratic:feature/uint256-op-modulus

Conversation

@thomas-quadratic
Copy link
Copy Markdown
Contributor

@thomas-quadratic thomas-quadratic commented Sep 18, 2025

PR description

This PR implements the MOD operation in the EVM using a custom UInt256 class meant to replace BigInteger for optimised arithmetics.

The UInt256 class should grow to contain other arithmetic operations, but as of now contains mod, computing the remainder of a division by the modulus. It should run about 45% faster than with BigIntegers (depending on integers' sizes). More optimisations should come later as well, but this should be already useful for early release.

Fixed Issue(s)

I did not find an opened issue for this task.

Thanks for sending a pull request! Have you done the following?

  • Checked out our contribution guidelines?
  • Considered documentation and added the doc-change-required label to this PR if updates are required.
  • Considered the changelog and included an update if required.
  • For database changes (e.g. KeyValueSegmentIdentifier) considered compatibility and performed forwards and backwards compatibility tests

Locally, you can run these tests to catch failures early:

  • spotless: ./gradlew spotlessApply
  • unit tests: ./gradlew build
  • acceptance tests: ./gradlew acceptanceTest
  • integration tests: ./gradlew integrationTest
  • reference tests: ./gradlew ethereum:referenceTests:referenceTests
  • hive tests: Engine or other RPCs modified?

@matkt matkt marked this pull request as draft September 19, 2025 05:28
@thomas-quadratic thomas-quadratic force-pushed the feature/uint256-op-modulus branch from c2876f7 to 83afdd4 Compare September 22, 2025 12:43
@ahamlat
Copy link
Copy Markdown
Contributor

ahamlat commented Sep 24, 2025

I propose adding a "fully random" case to the microbenchmarks. While it isn’t algorithmically meaningful, it can reveal real-world performance effects (branch prediction, cache behavior, allocation patterns) under heterogeneous inputs :
main...ahamlat:besu:feature/uint256-op-modulus-full-random#diff-bb4eb8b2c43632374a61d883bf5c0828013b6dc076898c846c58badba2ac3e22R53

@thomas-quadratic
Copy link
Copy Markdown
Contributor Author

I propose adding a "fully random" case to the microbenchmarks.
Yes, it was left in and separated the other tests in another benchmark class, but the way you add it in your PR is preferable in my opinion.
I will update the PR with your random case.

@ahamlat
Copy link
Copy Markdown
Contributor

ahamlat commented Sep 24, 2025

@thomas-quadratic Could you add this shortcut improvement
main...ahamlat:besu:feature/uint256-op-modulus-full-random#diff-5278aa622ca5723b7a5661875be3502a1f215537a805db99ed83aa1af51f7056R54

This will resolve the only regression I see from the benchmarks
image

@thomas-quadratic thomas-quadratic force-pushed the feature/uint256-op-modulus branch 2 times, most recently from fe159ea to 21a3e6a Compare September 30, 2025 17:34
Copy link
Copy Markdown
Contributor

@siladu siladu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I converted Nethermind's UInt256 tests to Java and found and fixed some bugs (bug fixes mentioned in suggestions): 62c7946 (#9264)
(to compare when the base branch changes https://github.com/hyperledger/besu/compare/thomas-quadratic:feature/uint256-op-modulus..siladu:uint256-op-modulus-parameterised-tests)

There was a recent regression in mod durign your last force push, before yesterday it was passing the parameterised testMod.

There are bugs in shiftLeft and shiftRight but these shouldn't get hit by valid mod inputs...however since they are public methods probably makes sense to fix them?

Finally, do we need a division by zero test for the mod methods?

@lu-pinto
Copy link
Copy Markdown
Contributor

lu-pinto commented Oct 3, 2025

Do you need any of these methods right now just for getting the opcodes going?

  • intValue
  • longValue
  • fromLong
  • fromInt

If not I would recommend removing them to not overcomplicate right now. They create API consistency problems and with a sign field approach they will require more work to get right and can be added later.

* @param sign padding with 0s or 1s whether sign is non-negative.
* @return HexString
*/
public String toString(final int sign) {
Copy link
Copy Markdown
Contributor

@lu-pinto lu-pinto Oct 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also can't see why you need to print UInt256 with a sign - plz remove

Copy link
Copy Markdown
Contributor

@siladu siladu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've ported over some more tests for SMod and pushed to #9264

https://github.com/hyperledger/besu/compare/thomas-quadratic:feature/uint256-op-modulus..siladu:uint256-op-modulus-parameterised-tests

Tests are passing ✅

Not in the PR, but I also tried porting over some of geth's uint256 tests and these were passing except for SMod. The failures were related to how UInt256 were constructed which is what led me to the comment about the signedMod input assumptions.

@thomas-quadratic thomas-quadratic force-pushed the feature/uint256-op-modulus branch from af9125a to 14774b9 Compare October 7, 2025 09:20
Signed-off-by: Thomas Zamojski <thomas.zamojski@quadratic-labs.com>
@siladu
Copy link
Copy Markdown
Contributor

siladu commented Oct 8, 2025

@thomas-quadratic I reran the tests from #9264 against your latest commits and testAdd and testLeftShift are failing now. Doesn't seem critical since they aren't actually used, but maybe we should either remove the public methods or fix...not sure these will be included in the fuzzing.

Comment on lines +262 to +268
Bytes32 expected =
BigInteger.ZERO.compareTo(big_modulus) == 0
? Bytes32.ZERO
: bigIntTo32B(big_number.mod(big_modulus));
assertThat(remainder).isEqualTo(expected);
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Bytes32 expected =
BigInteger.ZERO.compareTo(big_modulus) == 0
? Bytes32.ZERO
: bigIntTo32B(big_number.mod(big_modulus));
assertThat(remainder).isEqualTo(expected);
}
}
BigInteger expected = BigInteger.ZERO;
if (BigInteger.ZERO.compareTo(big_modulus) != 0) {
expected = big_number.mod(big_modulus);
}
assertThat(bytesToBigInt(remainder, 1)).isEqualTo(expected);
}
}

Comment on lines +279 to +282
Bytes32 expected =
BigInteger.ZERO.compareTo(mbig) == 0 ? Bytes32.ZERO : bigIntTo32B(xbig.add(ybig).mod(mbig));
assertThat(remainder).isEqualTo(expected);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Bytes32 expected =
BigInteger.ZERO.compareTo(mbig) == 0 ? Bytes32.ZERO : bigIntTo32B(xbig.add(ybig).mod(mbig));
assertThat(remainder).isEqualTo(expected);
}
BigInteger expected = BigInteger.ZERO;
if (BigInteger.ZERO.compareTo(mbig) != 0) {
expected = xbig.add(ybig).mod(mbig);
}
assertThat(bytesToBigInt(remainder, 1)).isEqualTo(expected);
}

Comment on lines +332 to +338
Bytes32 expected =
BigInteger.ZERO.compareTo(cInt) == 0
? Bytes32.ZERO
: bigIntTo32B(aInt.multiply(bInt).mod(cInt));
assertThat(remainder).isEqualTo(expected);
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Bytes32 expected =
BigInteger.ZERO.compareTo(cInt) == 0
? Bytes32.ZERO
: bigIntTo32B(aInt.multiply(bInt).mod(cInt));
assertThat(remainder).isEqualTo(expected);
}
}
BigInteger expected = BigInteger.ZERO;
if (BigInteger.ZERO.compareTo(cInt) != 0) {
expected = aInt.multiply(bInt).mod(cInt);
}
assertThat(bytesToBigInt(remainder, 1)).isEqualTo(expected);
}
}

Comment on lines +30 to +44
private Bytes32 bigIntTo32B(final BigInteger x) {
byte[] a = x.toByteArray();
if (a.length > 32) return Bytes32.wrap(a, a.length - 32);
return Bytes32.leftPad(Bytes.wrap(a));
}

private Bytes32 bigIntToSigned32B(final BigInteger x) {
if (x.signum() >= 0) return bigIntTo32B(x);
byte[] a = new byte[32];
Arrays.fill(a, (byte) 0xFF);
byte[] b = x.toByteArray();
System.arraycopy(b, 0, a, 32 - b.length, b.length);
if (a.length > 32) return Bytes32.wrap(a, a.length - 32);
return Bytes32.leftPad(Bytes.wrap(a));
}
Copy link
Copy Markdown
Contributor

@lu-pinto lu-pinto Oct 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is much easier to convert Bytes -> BigInteger than the reverse because BigInteger has better facilities to negate numbers and padding comes for free since BigInteger removes zeros anyway. You just need to figure out the sign.

So I would recommend removing the code here and replace it with:

  private static BigInteger bytesToBigInt(final Bytes bytes, final int sign) {
    // bytes can be shorter, so it's treated as left padded with zeros
    if (bytes.size() < 32) {
      return new BigInteger(1, bytes.toArrayUnsafe());
    }
    return sign < 0
      ? new BigInteger(bytes.toArrayUnsafe())
      : new BigInteger(1, bytes.toArrayUnsafe());
  }

Have been running tests indefinitely for an hour with the suggestions above and no failures.

@siladu
Copy link
Copy Markdown
Contributor

siladu commented Oct 10, 2025

Fix for add here thomas-quadratic#3

@thomas-quadratic thomas-quadratic force-pushed the feature/uint256-op-modulus branch from 6c36d5c to 0cdb13e Compare October 10, 2025 08:08
moodmosaic and others added 4 commits October 10, 2025 11:21
- shiftRight: wrong limb/index math for edge inputs
- shiftLeft: array index out of bounds on all-zero slice
- mod: knuth remainder misnormalizes, wrong indices/carries
- mod (1-limb): uses signed %, not unsigned

bigInteger is the reference model. failures are defects in
evm/UInt256.java.

Signed-off-by: Nikos Baxevanis <nikos.baxevanis@gmail.com>
Signed-off-by: Nikos Baxevanis <nikos.baxevanis@gmail.com>
Use Bytes32.leftPad to normalize inputs before constructing BigIntegers
and UInt256 values in property_signedMod_matchesEvmSemantics.

Avoids sign-extension and length mismatch issues for shorter byte arrays.

Signed-off-by: Nikos Baxevanis <nikos.baxevanis@gmail.com>
The shiftLeft operation did not make sure to provision enough space for the result of shiftLeftInto.
Now it does so, but it is somewhat wasteful at the moment, as max 8 limbs are going to be kept.
This should be optimised later on.

Signed-off-by: Thomas Zamojski <thomas.zamojski@quadratic-labs.com>
@thomas-quadratic thomas-quadratic force-pushed the feature/uint256-op-modulus branch from 0cdb13e to 98c67b0 Compare October 10, 2025 09:54
Signed-off-by: Thomas Zamojski <thomas.zamojski@quadratic-labs.com>
Signed-off-by: Thomas Zamojski <thomas.zamojski@quadratic-labs.com>
private final int[] limbs;
private final int length;

int[] limbs() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: add @VisibleForTesting

// --------------------------------------------------------------------------

UInt256(final int[] limbs, final int length) {
// Unchecked length: assumes limbs have length == N_LIMBS
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it an optimisation to leave unchecked?

Signed-off-by: Thomas Zamojski <thomas.zamojski@quadratic-labs.com>
@thomas-quadratic
Copy link
Copy Markdown
Contributor Author

Thanks everyone for your reviews, I think the API is stronger now.
We will deploy this PR, plus some of yours on top, and we will continue investigating optimisations, like long limbs, but also on int limbs.

@thomas-quadratic thomas-quadratic marked this pull request as ready for review October 13, 2025 15:09
@ahamlat ahamlat merged commit 0db2a0a into besu-eth:main Oct 13, 2025
46 checks passed
jflo pushed a commit to jflo/besu that referenced this pull request Oct 13, 2025
* ADD: UInt256 implementation of evm's op MOD

Signed-off-by: Thomas Zamojski <thomas.zamojski@quadratic-labs.com>
Signed-off-by: Nikos Baxevanis <nikos.baxevanis@gmail.com>
Co-authored-by: Nikos Baxevanis <nikos.baxevanis@gmail.com>
pinges pushed a commit to pinges/besu that referenced this pull request Dec 15, 2025
* ADD: UInt256 implementation of evm's op MOD

Signed-off-by: Thomas Zamojski <thomas.zamojski@quadratic-labs.com>
Signed-off-by: Nikos Baxevanis <nikos.baxevanis@gmail.com>
Co-authored-by: Nikos Baxevanis <nikos.baxevanis@gmail.com>
Signed-off-by: stefan.pingel@consensys.net <stefan.pingel@consensys.net>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants